#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <inttypes.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/select.h>
#include <pthread.h>
#include "progress.h"

#define MAX_CLIENTS		16

static void *conn_thread(void *arg);

static int lsock = -1;
static struct sockaddr_un addr;

static struct pclient {
	int s;
	struct pclient *next;
} *clist;

static pthread_mutex_t clist_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_t tid;

int progr_init(void)
{
	if((lsock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
		perror("failed to create progress socket");
		return -1;
	}
	
	memset(&addr, 0, sizeof addr);
	addr.sun_family = AF_UNIX;
	sprintf(addr.sun_path, "/tmp/rendy.%d.sock", (int)getpid());

	unlink(addr.sun_path);	/* in case it exists */

	if(bind(lsock, (struct sockaddr*)&addr, sizeof addr) == -1) {
		fprintf(stderr, "failed to bind progress socket: %s: %s\n", addr.sun_path, strerror(errno));
		close(lsock);
		return -1;
	}
	listen(lsock, 4);

	if(pthread_create(&tid, 0, conn_thread, 0) != 0) {
		fprintf(stderr, "thread creation failed\n");
		close(lsock);
		return -1;
	}

	signal(SIGPIPE, SIG_IGN);

	return 0;
}

void progr_cleanup(void)
{
	struct pclient *tmp, *c = clist;
	while(c) {
		tmp = c;
		c = c->next;

		close(tmp->s);
		free(tmp);
	}
	unlink(addr.sun_path);

	if(tid) {
		pthread_cancel(tid);
		pthread_join(tid, 0);
	}

	if(lsock >= 0) {
		close(lsock);
	}
}

void progress(int bidx, int p, int max)
{
	struct pclient *c, dummy;
	int need_removal = 0;
	uint16_t buf[2];

	buf[0] = (uint16_t)bidx;
	buf[1] = (uint16_t)(USHRT_MAX * p / (max - 1));

	c = clist;
	while(c) {
		if(write(c->s, buf, sizeof buf) == -1) {
			close(c->s);
			c->s = -1;
			need_removal = 1;
		}
		c = c->next;
	}
	
	if(need_removal) {
		pthread_mutex_lock(&clist_mutex);
		dummy.next = clist;
		c = &dummy;
		
		while(c->next) {
			if(c->next->s == -1) {
				struct pclient *tmp = c->next;
				c->next = c->next->next;
				
				if(clist == tmp) {
					clist = tmp->next;
				}

				free(tmp);
			} else {
				c = c->next;
			}
		}
		pthread_mutex_unlock(&clist_mutex);
	}
}

static void *conn_thread(void *arg)
{
	for(;;) {
		int s;
		struct pclient *c;

		/* Apparently on OSX, accept is not a cancellation point, while
		 * select is. So, we'll block inside select instead of blocking
		 * in accept.
		 * NOTE: something tells me that on OSX < 10.4, select won't be
		 * a proper cancellation point and I'll have to add a timeout to
		 * select and a repeated call to pthread_testcancel. Test that!
		 */
#if defined(__APPLE__) && defined(__MACH__)
		fd_set rdset;
		int nrd;

		FD_ZERO(&rdset);
		FD_SET(lsock, &rdset);

		if((nrd = select(lsock+1, &rdset, 0, 0, 0)) <= 0) {
			if(nrd == -1 && errno != EINTR) {
				perror("select failed");
			}
			continue;
		}
#endif
		if((s = accept(lsock, 0, 0)) == -1) {
			if(errno != EINTR) {
				perror("accept failed");
			}
			continue;
		}

		if(!(c = malloc(sizeof *c))) {
			perror("progress client connection failed");
			close(s);
			continue;
		}
		c->s = s;

		pthread_mutex_lock(&clist_mutex);
		c->next = clist;
		clist = c;
		pthread_mutex_unlock(&clist_mutex);
	}
}
